/* Copyright 2020 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==============================================================================*/
import { customElement, observe, property } from '@polymer/decorators';
import { html, PolymerElement } from '@polymer/polymer';
import '../polymer/irons_and_papers';
import { LegacyElementMixin } from '../polymer/legacy_element_mixin';
import '../tf_graph/tf-graph';
import * as tf_graph from '../tf_graph_common/graph';
import * as tf_graph_hierarchy from '../tf_graph_common/hierarchy';
import * as tf_graph_render from '../tf_graph_common/render';
import '../tf_graph_info/tf-graph-info';

/**
 * Element for putting tf-graph and tf-graph-info side by side.
 *
 * Example
 * <tf-graph-board graph=[[graph]]></tf-graph-board>
 */
@customElement('tf-graph-board')
class TfGraphBoard extends LegacyElementMixin(PolymerElement) {
  static readonly template = html`
    <style>
      ::host {
        display: block;
      }

      /deep/ .close {
        position: absolute;
        cursor: pointer;
        left: 15px;
        bottom: 15px;
      }

      .container {
        width: 100%;
        height: 100%;
        opacity: 1;
      }

      .container.loading {
        cursor: progress;
        opacity: 0.1;
      }

      .container.loading.error {
        cursor: auto;
      }

      #info {
        position: absolute;
        right: 5px;
        top: 5px;
        padding: 0px;
        min-width: 400px;
        background-color: rgba(255, 255, 255, 0.9);
        @apply --shadow-elevation-2dp;
      }

      #graph-info {
        position: relative;
        right: 0px;
        top: 0px;
        max-width: 1000px;
        max-height: 90vh;
        min-width: 400px;
        padding: 0px;
      }

      .resize-handle {
        position: absolute;
        bottom: 0;
        left: 0;
        width: 20px;
        height: 20px;
        cursor: ne-resize;
        z-index: 1;
      }

      #main {
        width: 100%;
        height: 100%;
      }

      #progress-bar {
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        width: 100%;
        position: absolute;
        top: 40px;
        left: 0;
        font-size: 13px;
      }

      #progress-msg {
        margin-bottom: 5px;
        white-space: pre-wrap;
        width: 400px;
      }

      paper-progress {
        width: 400px;
        --paper-progress-height: 6px;
        --paper-progress-active-color: #f3913e;
      }

      .context-menu {
        position: absolute;
        display: none;
        background-color: #e2e2e2;
        border-radius: 2px;
        font-size: 14px;
        min-width: 150px;
        border: 1px solid #d4d4d4;
      }

      /deep/ .context-menu ul {
        list-style-type: none;
        margin: 0;
        padding: 0;
        cursor: default;
      }

      /deep/ .context-menu ul li {
        padding: 4px 16px;
      }

      /deep/ .context-menu ul li:hover {
        background-color: #f3913e;
        color: white;
      }
    </style>
    <template is="dom-if" if="[[_isNotComplete(progress)]]">
      <div id="progress-bar">
        <div id="progress-msg">[[progress.msg]]</div>
        <paper-progress value="[[progress.value]]"></paper-progress>
      </div>
    </template>
    <div class$="[[_getContainerClass(progress)]]">
      <div id="main">
        <tf-graph
          id="graph"
          selection="[[selection]]"
          graph-hierarchy="{{graphHierarchy}}"
          basic-graph="[[graph]]"
          hierarchy-params="[[hierarchyParams]]"
          render-hierarchy="{{renderHierarchy}}"
          devices-for-stats="[[devicesForStats]]"
          stats="[[stats]]"
          selected-node="{{selectedNode}}"
          selected-edge="{{selectedEdge}}"
          highlighted-node="{{_highlightedNode}}"
          progress="{{progress}}"
          edge-label-function="[[edgeLabelFunction]]"
          edge-width-function="[[edgeWidthFunction]]"
          handle-node-selected="[[handleNodeSelected]]"
          handle-edge-selected="[[handleEdgeSelected]]"
          trace-inputs="[[traceInputs]]"
          menu="[[menu]]"
          colorset="[[colorset]]"
          auto-extract-nodes="[[autoExtractNodes]]"
        ></tf-graph>
      </div>
      <div id="info">
        <tf-graph-info
          id="graph-info"
          title="selected"
          graph-hierarchy="[[graphHierarchy]]"
          render-hierarchy="[[renderHierarchy]]"
          graph="[[graph]]"
          selected-node="{{selectedNode}}"
          selected-edge="{{selectedEdge}}"
          selected-node-include="{{_selectedNodeInclude}}"
          highlighted-node="{{_highlightedNode}}"
          debugger-data-enabled="[[debuggerDataEnabled]]"
          debugger-numeric-alerts="[[debuggerNumericAlerts]]"
          tooltips="[[tooltips]]"
          all-steps-mode-enabled="{{allStepsModeEnabled}}"
          compat-node-title="[[compatNodeTitle]]"
          on-node-toggle-inclusion="_onNodeInclusionToggled"
          on-node-toggle-seriesgroup="_onNodeSeriesGroupToggled"
        >
        </tf-graph-info>
        <div class="resize-handle" id="resize-handle">
          <svg width="20px" height="20px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path
              opacity="0.5"
              d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12Z"
              fill="#1C274C"
            />
            <path
              fill-rule="evenodd"
              clip-rule="evenodd"
              d="M14 7.75C13.5858 7.75 13.25 7.41421 13.25 7C13.25 6.58579 13.5858 6.25 14 6.25H17C17.4142 6.25 17.75 6.58579 17.75 7V10C17.75 10.4142 17.4142 10.75 17 10.75C16.5858 10.75 16.25 10.4142 16.25 10V8.81066L14.0303 11.0303C13.7374 11.3232 13.2626 11.3232 12.9697 11.0303C12.6768 10.7374 12.6768 10.2626 12.9697 9.96967L15.1893 7.75H14ZM11.0303 12.9697C11.3232 13.2626 11.3232 13.7374 11.0303 14.0303L8.81066 16.25H10C10.4142 16.25 10.75 16.5858 10.75 17C10.75 17.4142 10.4142 17.75 10 17.75H7C6.58579 17.75 6.25 17.4142 6.25 17V14C6.25 13.5858 6.58579 13.25 7 13.25C7.41421 13.25 7.75 13.5858 7.75 14V15.1893L9.96967 12.9697C10.2626 12.6768 10.7374 12.6768 11.0303 12.9697Z"
              fill="#1C274C"
            />
          </svg>
        </div>
      </div>
    </div>
  `;
  @property({ type: Object })
  graphHierarchy: tf_graph_hierarchy.Hierarchy;
  @property({ type: Object })
  graph: tf_graph.SlimGraph;
  // TODO(psybuzz): ideally, this would be a required property and the component
  // that owns <tf-graph-board> and the graph loader should create these params.
  @property({ type: Object })
  hierarchyParams: tf_graph_hierarchy.HierarchyParams = tf_graph_hierarchy.DefaultHierarchyParams;
  @property({ type: Object })
  stats: object;
  /**
   * A number between 0 and 100 denoting the % of progress
   * for the progress bar and the displayed message.
   * @type {{value: number, msg: string}}
   */
  @property({ type: Object })
  progress: object;
  @property({ type: Boolean })
  traceInputs: boolean;
  @property({ type: Boolean })
  autoExtractNodes: boolean;
  @property({
    type: Object,
    notify: true,
  })
  renderHierarchy: tf_graph_render.RenderGraphInfo;
  // Whether debugger data is enabled for this instance of Tensorboard.
  @property({ type: Boolean })
  debuggerDataEnabled: boolean;
  @property({
    type: Array,
    notify: true,
  })
  // An array of alerts (in chronological order) provided by debugging libraries on when bad
  // values (NaN, +/- Inf) appear.
  debuggerNumericAlerts: unknown[];
  @property({
    type: Boolean,
    notify: true,
  })
  allStepsModeEnabled: boolean = false;
  @property({
    type: Object,
  })
  menu: any;
  @property({
    type: Object,
  })
  colorset: any;
  @property({
    type: String,
    notify: true,
  })
  selectedNode: string;
  @property({
    type: Object,
    notify: true,
  })
  selectedEdge: tf_graph_render.EdgeData;
  @property({
    type: String,
  })
  compatNodeTitle: string = 'TPU Compatibility';
  // A function with signature EdgeThicknessFunction that computes the
  // thickness of a given edge.
  @property({ type: Object })
  edgeWidthFunction: object;
  // The enum value of the include property of the selected node.
  @property({ type: Number })
  _selectedNodeInclude: number;
  @property({ type: String })
  _highlightedNode: string;
  // An optional function that takes a node selected event (whose `detail`
  // property is the selected node ... which could be null if a node is
  // deselected). Called whenever a node is selected or deselected.
  @property({ type: Object })
  handleNodeSelected: object;
  // An optional function that computes the label for an edge. Should
  // implement the EdgeLabelFunction signature.
  @property({ type: Object })
  edgeLabelFunction: object;
  // An optional callback that implements the tf.graph.edge.EdgeSelectionCallback signature. If
  // provided, edges are selectable, and this callback is run when an edge is selected.
  @property({ type: Object })
  handleEdgeSelected: object;
  @property({ type: Object })
  selection: object;
  @property({ type: Object })
  tooltips: object;

  ready() {
    super.ready();

    const handle = this.shadowRoot?.getElementById('resize-handle') as HTMLElement;

    if (handle) {
      let isResizing = false;
      let lastDownX = 0;
      let lastDownY = 0;

      handle.addEventListener('mousedown', (e: MouseEvent) => {
        e.preventDefault();
        isResizing = true;
        lastDownX = e.clientX;
        lastDownY = e.clientY;
        document.addEventListener('mousemove', handleMouseMove);
        document.addEventListener('mouseup', handleMouseUp);
      });

      const handleMouseMove = (e: MouseEvent) => {
        if (isResizing) {
          const graphInfoDiv = this.shadowRoot?.getElementById('graph-info') as HTMLElement;
          if (graphInfoDiv) {
            const rect = graphInfoDiv.getBoundingClientRect();
            const width = rect.width + (lastDownX - e.clientX);
            const height = rect.height + (e.clientY - lastDownY);
            graphInfoDiv.style.width = `${Math.max(width, 100)}px`;
            graphInfoDiv.style.height = `${Math.max(height, 65)}px`;
            lastDownX = e.clientX;
            lastDownY = e.clientY;
          }
        }
      };

      const handleMouseUp = () => {
        isResizing = false;
        document.removeEventListener('mousemove', handleMouseMove);
        document.removeEventListener('mouseup', handleMouseUp);
      };
    }
  }
  fit() {
    (this.$.graph as any).fit();
  }
  async downloadAsImage(filename: string) {
    const blob = await (this.$.graph as any).getImageBlob();
    const element = document.createElement('a');
    (element as any).href = (URL as any).createObjectURL(blob);
    element.download = filename;
    element.click();
    URL.revokeObjectURL(element.href);
  }
  /** True if the progress is not complete yet (< 100 %). */
  _isNotComplete(progress) {
    return progress.value < 100;
  }
  _getContainerClass(progress) {
    var result = 'container';
    if (progress.error) {
      result += ' error';
    }
    if (this._isNotComplete(progress)) {
      result += ' loading';
    }
    return result;
  }
  _onNodeInclusionToggled(event) {
    (this.$.graph as any).nodeToggleExtract(event.detail.name);
  }
  _onNodeSeriesGroupToggled(event) {
    (this.$.graph as any).nodeToggleSeriesGroup(event.detail.name);
  }
  @observe('selectedNode', 'renderHierarchy')
  _updateNodeInclude() {
    const node = !this.renderHierarchy ? null : this.renderHierarchy.getNodeByName(this.selectedNode);
    this._selectedNodeInclude = node ? node.include : tf_graph.InclusionType.UNSPECIFIED;
  }
}
